// Copyright (C) 2025 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import m from 'mithril';
import {SqlValue} from '../../../trace_processor/query_result';

export type AggregationFunction = 'SUM' | 'AVG' | 'COUNT' | 'MIN' | 'MAX';

export type FilterType = DataGridFilter['op'];

export const DEFAULT_SUPPORTED_FILTERS: ReadonlyArray<FilterType> = [
  '=',
  '!=',
  '<',
  '<=',
  '>',
  '>=',
  'glob',
  'not glob',
  'in',
  'not in',
  'is null',
  'is not null',
];

export type CellRenderer = (value: SqlValue, row: RowDef) => m.Children;
export type CellFormatter = (value: SqlValue, row: RowDef) => string;

export interface ColumnDefinition {
  // Name/id of the column - this should match the key in the data.
  readonly name: string;

  // Human readable title to display instead of the name.
  readonly title?: m.Children;

  // Custom renderer for this column's cells
  readonly cellRenderer?: CellRenderer;

  // Optional value formatter for this column. This is used when exporting
  // data to format the value as a string.
  readonly cellFormatter?: CellFormatter;

  // An optional aggregation for data in this column displayed in the header
  // bar.
  readonly aggregation?: AggregationFunction;

  // Optional function that receives default menu item groups and returns
  // the complete menu structure. This allows full control over menu organization.
  // Default groups provided:
  // - sorting: Sort ascending/descending/clear items
  // - filters: Filter options (null filters, equals, contains, etc.)
  // - fitToContent: Fit column to content width
  // - columnManagement: Hide column, manage columns visibility
  readonly contextMenuRenderer?: (builtins: {
    readonly sorting?: m.Children;
    readonly filters?: m.Children;
    readonly fitToContent?: m.Children;
    readonly columnManagement?: m.Children;
  }) => m.Children;

  // Optional function that receives the default filter menu item and returns
  // the complete cell context menu structure. This allows full control over
  // the cell menu organization.
  // Default item provided:
  // - addFilter: "Add filter..." menu item with context-sensitive filter options
  readonly cellContextMenuRenderer?: (
    value: SqlValue,
    row: RowDef,
    builtins: {
      addFilter?: m.Children;
    },
  ) => m.Children;

  // Enable distinct values filtering for this column. When enabled, adds a
  // "Filter by values..." menu item that shows all distinct values. Only
  // enable for columns with low cardinality (e.g., strings, enums).
  readonly distinctValues?: boolean;

  // Control which types of filters are available for this column.
  // - 'numeric': Shows comparison filters (=, !=, <, <=, >, >=) and null filters
  // - 'string': Shows text filters (contains, glob) and equals/null filters
  // - undefined: Shows all applicable filters based on other settings
  readonly filterType?: 'numeric' | 'string';
}

export interface FilterValue {
  readonly column: string;
  readonly op: '=' | '!=' | '<' | '<=' | '>' | '>=' | 'glob' | 'not glob';
  readonly value: SqlValue;
}

export interface FilterIn {
  readonly column: string;
  readonly op: 'in' | 'not in';
  readonly value: ReadonlyArray<SqlValue>;
}

export interface FilterNull {
  readonly column: string;
  readonly op: 'is null' | 'is not null';
}

export type DataGridFilter = FilterValue | FilterNull | FilterIn;

export interface SortByColumn {
  readonly column: string;
  readonly direction: 'ASC' | 'DESC';
}

export interface Unsorted {
  readonly direction: 'UNSORTED';
}

export type Sorting = SortByColumn | Unsorted;

export interface DataSourceResult {
  readonly totalRows: number;
  readonly rowOffset: number;
  readonly rows: ReadonlyArray<RowDef>;
  readonly aggregates: RowDef;
  readonly isLoading?: boolean;
  readonly distinctValues?: ReadonlyMap<string, readonly SqlValue[]>;
}

export type RowDef = {[key: string]: SqlValue};

export interface Pagination {
  readonly offset: number;
  readonly limit: number;
}

export interface AggregateSpec {
  readonly col: string;
  readonly func: AggregationFunction;
}

export interface DataGridModel {
  readonly columns?: ReadonlyArray<string>;
  readonly sorting?: Sorting;
  readonly filters?: ReadonlyArray<DataGridFilter>;
  readonly pagination?: Pagination;
  readonly aggregates?: ReadonlyArray<AggregateSpec>;
  readonly distinctValuesColumns?: ReadonlySet<string>;
}

export interface DataGridDataSource {
  readonly rows?: DataSourceResult;
  readonly isLoading?: boolean;
  notifyUpdate(model: DataGridModel): void;

  /**
   * Export all data with current filters/sorting applied.
   * Returns a promise that resolves to all filtered and sorted rows.
   */
  exportData(): Promise<readonly RowDef[]>;
}

/**
 * Compares two arrays of AggregateSpec objects for equality.
 *
 * Two arrays are considered equal if they have the same length and each
 * corresponding AggregateSpec object is equal.
 */
export function areAggregateArraysEqual(
  a?: ReadonlyArray<AggregateSpec>,
  b?: ReadonlyArray<AggregateSpec>,
): boolean {
  // Both undefined or same object- they're equal
  if (a === b) return true;

  // One is undefined, other isn't - they're different
  if (!a || !b) return false;

  // Lengths differ - they're different
  if (a.length !== b.length) return false;

  // Check each element
  for (let i = 0; i < a.length; i++) {
    if (!areAggregatesEqual(a[i], b[i])) {
      return false;
    }
  }

  // All elements are the same
  return true;
}

/**
 * Compares two AggregateSpec objects for equality.
 *
 * Two AggregateSpec objects are considered equal if their `col` and `func`
 * properties are the same.
 */
export function areAggregatesEqual(
  a: AggregateSpec,
  b: AggregateSpec,
): boolean {
  return a.col === b.col && a.func === b.func;
}
